package net.butov.wrappers;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Maxim Butov
 * @version $Id$
 */
public class Utils {

    public static Class[] getTypes(Object... args) {
        Class[] types = new Class[args.length];

        for (int k = 0; k < args.length; ++k) {
            Object arg = args[k];
            if (arg != null) {
                types[k] = arg.getClass();
            }
        }

        return types;
    }

    public static boolean hasNulls(Object... objects) {
        for (Object obj : objects) {
            if (obj == null) {
                return true;
            }
        }
        return false;
    }

    public static Class[] primitivesToWrappers(Class[] classes) {
        Class[] wrapperClasses = new Class[classes.length];
        for (int k = 0; k < classes.length; ++k) {
            wrapperClasses[k] = primitiveToWrapper(classes[k]);
        }
        return wrapperClasses;
    }

    public static final Map<Class, Class> PRIMITIVES_WRAPPERS = Collections.unmodifiableMap(
            new HashMap<Class, Class>() {
                {
                    put(boolean.class, Boolean.class);
                    put(byte.class, Byte.class);
                    put(char.class, Character.class);
                    put(short.class, Short.class);
                    put(int.class, Integer.class);
                    put(long.class, Long.class);
                    put(float.class, Float.class);
                    put(double.class, Double.class);
                }
            }
    );

    public static final Map<Class, Class> WRAPPERS_PRIMITIVES = Collections.unmodifiableMap(
            new HashMap<Class, Class>() {
                {
                    for (Class primitive : PRIMITIVES_WRAPPERS.keySet()) {
                        put(PRIMITIVES_WRAPPERS.get(primitive), primitive);
                    }
                }
            }
    );

    public static final Map<Class, Object> PRIMITIVE_DEFAULT_VALUES = Collections.unmodifiableMap(
            new HashMap<Class, Object>() {
                {
                    for (Class primitive : PRIMITIVES_WRAPPERS.keySet()) {
                        put(primitive, Array.get(Array.newInstance(primitive, 1), 0));
                    }
                }
            }
    );

    /**
     * Classes with lower indexes are assignable from classes with higher indexes
     */
    public static final List<Class> ASSIGNABILITY = Collections.unmodifiableList(Arrays.asList(
            (Class) Double.class, Float.class, Long.class, Integer.class, Short.class, Byte.class
    ));

    public static <T> Class<T> primitiveToWrapper(Class<T> clazz) {
        return translate(clazz, PRIMITIVES_WRAPPERS);
    }

    public static <T> Class<T> wrapperToPrimitive(Class<T> clazz) {
        return translate(clazz, WRAPPERS_PRIMITIVES);
    }

    public static <T> Class<T> translate(Class<T> clazz, Map<Class, Class> map) {
        return map.containsKey(clazz) ? map.get(clazz) : clazz;
    }

    public static boolean isClassAssignableFrom(Class base, Class descendant) {
        return isClassAssignableFrom(base, descendant, false);
    }

    public static boolean isClassAssignableFrom(Class base, Class descendant, boolean isPrimitiveAssignableFromWrapper) {
        if (base == null) {

            return descendant == null;

        } else if (descendant == null) {

            return !base.isPrimitive();

        } else {

            if (base.isAssignableFrom(descendant)) {
                return true;
            }

            if (!isPrimitiveAssignableFromWrapper && base.isPrimitive() && !descendant.isPrimitive()) {
                return false;
            }

            Class baseWrapper = primitiveToWrapper(base);
            Class descendantWrapper = primitiveToWrapper(descendant);

            int baseIndex = ASSIGNABILITY.indexOf(baseWrapper);
            int descendantIndex =
                    ASSIGNABILITY.indexOf(Character.class.equals(descendantWrapper) ? Short.class : descendantWrapper);

            if (baseIndex >= 0 && descendantIndex >= 0) {
                return baseIndex <= descendantIndex;
            } else {
                return baseWrapper.isAssignableFrom(descendantWrapper);
            }
        }
    }

    public static boolean isClassesAssignableFrom(Class[] bases, Class[] descendants) {
        return isClassesAssignableFrom(bases, descendants, false);
    }

    public static boolean isClassesAssignableFrom(Class[] bases, Class[] descendants,
            boolean isPrimitiveAssignableFromWrapper) {
        int length = bases.length;
        if (descendants.length != length) {
            throw new IllegalArgumentException();
        }
        for (int k = 0; k < length; ++k) {
            if (!isClassAssignableFrom(bases[k], descendants[k], isPrimitiveAssignableFromWrapper)) {
                return false;
            }
        }
        return true;
    }

    public static <T> T cast(Object obj, Class<? extends T> clazz) {
        try {
            return primitiveToWrapper(clazz).cast(obj);
        } catch (ClassCastException e) {

            if (Character.class.isInstance(obj)) {
                obj = (int) ((Character) obj).charValue();
            }

            int i1 = ASSIGNABILITY.indexOf(obj.getClass());
            int i2 = ASSIGNABILITY.indexOf(clazz);

            if (i1 < 0 || i2 < 0) {
                throw e;
            }

            Number number = (Number) obj;

            if (Double.class.equals(clazz)) {
                number = number.doubleValue();
            } else if (Float.class.equals(clazz)) {
                number = number.floatValue();
            } else if (Long.class.equals(clazz)) {
                number = number.longValue();
            } else if (Integer.class.equals(clazz)) {
                number = number.intValue();
            } else if (Short.class.equals(clazz)) {
                number = number.shortValue();
            } else if (Byte.class.equals(clazz)) {
                number = number.byteValue();
            } else {
                throw new RuntimeException("Unreachable");
            }

            return (T) number;
        }
    }

    public static final Comparator<Class[]> TYPES_COMPARATOR = new Comparator<Class[]>() {
        public int compare(Class[] types1, Class[] types2) {
            if (Arrays.equals(types1, types2)) {
                throw new IllegalArgumentException("Type sequences must not be equal");
            }
            boolean isAssignable12 = isClassesAssignableFrom(types1, types2, false);
            boolean isAssignable21 = isClassesAssignableFrom(types2, types1, false);
            if (!(isAssignable12 ^ isAssignable21)) {
                throw new IllegalArgumentException("Type sequences are not comparable");
            }
            return isAssignable12 ? +1 : -1;
        }
    };

    public static final Comparator<Method> METHOD_COMPARATOR = new Comparator<Method>() {
        public int compare(Method m1, Method m2) {
            try {
                return TYPES_COMPARATOR.compare(m1.getParameterTypes(), m2.getParameterTypes());
            } catch (Exception e) {
                throw new IllegalArgumentException("Can't compare methods " + m1 + " and " + m2);
            }
        }
    };

    public static final Comparator<Constructor> CONSTRUCTOR_COMPARATOR = new Comparator<Constructor>() {
        public int compare(Constructor c1, Constructor c2) {
            try {
                return TYPES_COMPARATOR.compare(c1.getParameterTypes(), c2.getParameterTypes());
            } catch (Exception e) {
                throw new IllegalArgumentException("Can't compare constructors " + c1 + " and " + c2);
            }
        }
    };

    public static Method getMethodByArguments(Class clazz, String name, Object... args) {
        return getMethodByTypes(clazz, name, getTypes(args));
    }

    public static Method getMethodByTypes(Class clazz, String name, Class... types) {

        if (!hasNulls(types)) {
            for (Class c = clazz; c != null; c = c.getSuperclass()) {
                try {
                    return c.getDeclaredMethod(name, types);
                } catch (NoSuchMethodException e) {
                    // continue
                }
            }
        }

        List<Method> candidates = new ArrayList<Method>();

        for (Class c = clazz; c != null; c = c.getSuperclass()) {
            for (Method m : c.getDeclaredMethods()) {
                if (name.equals(m.getName()) && isClassesAssignableFrom(m.getParameterTypes(), types, true)) {
                    candidates.add(m);
                }
            }
        }

        switch (candidates.size()) {
            case 0:
                return null;
            default:
                Collections.sort(candidates, METHOD_COMPARATOR);
            case 1:
                return candidates.get(0);
        }
    }

    public static Constructor getConstructorByArguments(Class clazz, Object... args) {
        return getConstructorByTypes(clazz, getTypes(args));
    }

    public static Constructor getConstructorByTypes(Class clazz, Class... types) {
        if (!hasNulls(types)) {
            try {
                return clazz.getDeclaredConstructor(types);
            } catch (NoSuchMethodException e) {
                // continue
            }
        }

        List<Constructor> candidates = new ArrayList<Constructor>();

        for (Constructor c : clazz.getDeclaredConstructors()) {
            if (isClassesAssignableFrom(c.getParameterTypes(), types, true)) {
                candidates.add(c);
            }
        }

        switch (candidates.size()) {
            case 0:
                return null;
            default:
                Collections.sort(candidates, CONSTRUCTOR_COMPARATOR);
            case 1:
                return candidates.get(0);
        }
    }

    public static boolean isOverriden(Method base, Method overriden) {
        return
                base.equals(overriden) || (
                        base.getName().equals(overriden.getName()) &&
                                Arrays.equals(base.getParameterTypes(), overriden.getParameterTypes()) &&
                                base.getDeclaringClass().isAssignableFrom(overriden.getDeclaringClass())
                );
    }

    private Utils() {
    }
}
